"""
    FortranBinaryIO

Low-level Fortran binary I/O utilities for GSI diagnostic file compatibility.

Fortran sequential unformatted files use a specific binary format:
- Each record is preceded and followed by a 4-byte integer (record length)
- Data is stored in native byte order (typically big-endian for GSI)
- Arrays are stored in column-major order
- REAL(4) = Float32, REAL(8) = Float64, INTEGER(4) = Int32

This module provides read/write functions compatible with Fortran's sequential
unformatted access.
"""
module FortranBinaryIO

using Printf

export FortranFile, open_fortran_file, close_fortran_file
export write_fortran_record, read_fortran_record, write_fortran_header, read_fortran_header
export write_fortran_string, read_fortran_string
export set_byte_order!, get_byte_order

# Byte order control
const BYTE_ORDER = Ref{Symbol}(:BigEndian)  # GSI typically uses big-endian

"""
    set_byte_order!(order::Symbol)

Set byte order for Fortran binary I/O.
- :BigEndian - Most common for GSI
- :LittleEndian - For some systems
- :Native - Use system native order
"""
function set_byte_order!(order::Symbol)
    if order ∉ [:BigEndian, :LittleEndian, :Native]
        error("Invalid byte order. Use :BigEndian, :LittleEndian, or :Native")
    end
    BYTE_ORDER[] = order
end

"""
    get_byte_order()

Get current byte order setting.
"""
get_byte_order() = BYTE_ORDER[]

"""
    FortranFile

Wrapper around IOStream for Fortran-compatible binary I/O.
"""
mutable struct FortranFile
    io::IOStream
    path::String
    mode::String
    byte_order::Symbol
    records_written::Int
    records_read::Int
end

"""
    open_fortran_file(path::String, mode::String="r"; byte_order::Symbol=:BigEndian)

Open a Fortran sequential unformatted file.

# Arguments
- `path`: File path
- `mode`: "r" for read, "w" for write, "a" for append
- `byte_order`: :BigEndian (default), :LittleEndian, or :Native

# Returns
- `FortranFile` object
"""
function open_fortran_file(path::String, mode::String="r"; byte_order::Symbol=:BigEndian)
    io_mode = mode == "r" ? "r" : (mode == "a" ? "a" : "w")
    io = open(path, io_mode)
    return FortranFile(io, path, mode, byte_order, 0, 0)
end

"""
    close_fortran_file(ff::FortranFile)

Close a Fortran file.
"""
function close_fortran_file(ff::FortranFile)
    close(ff.io)
    println("Closed Fortran file: $(ff.path)")
    println("  Records written: $(ff.records_written)")
    println("  Records read: $(ff.records_read)")
end

"""
    write_record_marker(ff::FortranFile, nbytes::Int32)

Write a Fortran record marker (4-byte integer).
"""
function write_record_marker(ff::FortranFile, nbytes::Int32)
    if ff.byte_order == :BigEndian
        write(ff.io, hton(nbytes))
    elseif ff.byte_order == :LittleEndian
        write(ff.io, htol(nbytes))
    else
        write(ff.io, nbytes)
    end
end

"""
    read_record_marker(ff::FortranFile)

Read a Fortran record marker (4-byte integer).
"""
function read_record_marker(ff::FortranFile)
    marker_bytes = read(ff.io, 4)
    if length(marker_bytes) < 4
        return nothing
    end

    marker = reinterpret(Int32, marker_bytes)[1]

    if ff.byte_order == :BigEndian
        marker = ntoh(marker)
    elseif ff.byte_order == :LittleEndian
        marker = ltoh(marker)
    end

    return marker
end

"""
    write_fortran_record(ff::FortranFile, data::Array{T}) where T

Write a Fortran sequential unformatted record.

The data array is automatically transposed to column-major order if needed.
Record markers are added before and after the data.

# Arguments
- `ff`: FortranFile object
- `data`: Data array to write (any numeric type or String)
"""
function write_fortran_record(ff::FortranFile, data::Array{T}) where T
    # Calculate record length in bytes
    nbytes = Int32(sizeof(data))

    # Write opening record marker
    write_record_marker(ff, nbytes)

    # Write data with appropriate byte order
    if ff.byte_order == :BigEndian
        if T <: Union{Int16, Int32, Int64, Float32, Float64}
            # Convert to big-endian
            for val in data
                write(ff.io, hton(val))
            end
        else
            write(ff.io, data)
        end
    elseif ff.byte_order == :LittleEndian
        if T <: Union{Int16, Int32, Int64, Float32, Float64}
            # Convert to little-endian
            for val in data
                write(ff.io, htol(val))
            end
        else
            write(ff.io, data)
        end
    else
        write(ff.io, data)
    end

    # Write closing record marker
    write_record_marker(ff, nbytes)

    ff.records_written += 1
end

"""
    write_fortran_record(ff::FortranFile, data::T) where T

Write a single scalar value as a Fortran record.
"""
function write_fortran_record(ff::FortranFile, data::T) where T <: Number
    write_fortran_record(ff, [data])
end

"""
    read_fortran_record(ff::FortranFile, T::Type=UInt8)

Read a Fortran sequential unformatted record.

# Arguments
- `ff`: FortranFile object
- `T`: Data type to read (default: UInt8 for raw bytes)

# Returns
- Array of type T, or nothing if end of file
"""
function read_fortran_record(ff::FortranFile, T::Type=UInt8)
    # Read opening record marker
    marker1 = read_record_marker(ff)
    if marker1 === nothing
        return nothing
    end

    # Read data
    nbytes = Int(marker1)
    data_bytes = read(ff.io, nbytes)

    # Read closing record marker
    marker2 = read_record_marker(ff)

    if marker1 != marker2
        @warn "Record markers don't match: $marker1 != $marker2"
    end

    ff.records_read += 1

    # Convert bytes to requested type if needed
    if T == UInt8
        return data_bytes
    else
        # Reinterpret as array of T
        n_elements = nbytes ÷ sizeof(T)
        data = reinterpret(T, data_bytes)[1:n_elements]

        # Convert from big/little endian if needed
        if ff.byte_order == :BigEndian && T <: Union{Int16, Int32, Int64, Float32, Float64}
            data = ntoh.(data)
        elseif ff.byte_order == :LittleEndian && T <: Union{Int16, Int32, Int64, Float32, Float64}
            data = ltoh.(data)
        end

        return data
    end
end

"""
    write_fortran_string(ff::FortranFile, s::String, length::Int)

Write a fixed-length Fortran string.

# Arguments
- `ff`: FortranFile object
- `s`: String to write
- `length`: Fixed length (padded with spaces if s is shorter, truncated if longer)
"""
function write_fortran_string(ff::FortranFile, s::String, length::Int)
    # Pad or truncate to specified length
    if Base.length(s) < length
        s = rpad(s, length)
    elseif Base.length(s) > length
        s = s[1:length]
    end

    # Convert to bytes
    bytes = Vector{UInt8}(s)

    # Write as part of current record (caller manages record markers)
    write(ff.io, bytes)
end

"""
    read_fortran_string(data::Vector{UInt8}, offset::Int, length::Int)

Read a fixed-length Fortran string from a byte array.

# Arguments
- `data`: Byte array
- `offset`: Starting offset (1-indexed)
- `length`: String length

# Returns
- String (right-trimmed of spaces)
"""
function read_fortran_string(data::Vector{UInt8}, offset::Int, length::Int)
    str_bytes = data[offset:offset+length-1]
    s = String(str_bytes)
    return rstrip(s)
end

"""
    write_fortran_header(ff::FortranFile, var_name::String, var_type::String)

Write a standard GSI diagnostic file header.

# Arguments
- `ff`: FortranFile object
- `var_name`: Variable name (e.g., "ps", "t", "q")
- `var_type`: Variable type descriptor
"""
function write_fortran_header(ff::FortranFile, var_name::String, var_type::String)
    # GSI header format: two 10-character strings + optional metadata
    header = Vector{UInt8}(undef, 38)

    # First string (variable name, padded to 10 chars)
    var_name_pad = rpad(var_name, 10)[1:10]
    header[1:10] = Vector{UInt8}(var_name_pad)

    # Second string (variable type, padded to 10 chars)
    var_type_pad = rpad(var_type, 10)[1:10]
    header[11:20] = Vector{UInt8}(var_type_pad)

    # Padding/metadata (18 bytes)
    header[21:38] .= 0x20  # Space character

    # Metadata section (usually contains grid info)
    # For now, using default values
    metadata = zeros(Int32, 5)
    header[21:24] .= 0x00
    header[25:28] = reinterpret(UInt8, [Int32(20)])  # Some grid dimension

    write_fortran_record(ff, header)
end

"""
    read_fortran_header(ff::FortranFile)

Read a GSI diagnostic file header.

# Returns
- Tuple of (var_name, var_type, metadata)
"""
function read_fortran_header(ff::FortranFile)
    header = read_fortran_record(ff, UInt8)

    if header === nothing || length(header) < 20
        return nothing
    end

    var_name = String(header[1:10]) |> rstrip
    var_type = String(header[11:20]) |> rstrip

    metadata = length(header) > 20 ? header[21:end] : UInt8[]

    return (var_name, var_type, metadata)
end

"""
    fortran_column_major(A::Array{T, N}) where {T, N}

Convert Julia row-major array to Fortran column-major order.

For 2D arrays, this is a transpose. For higher dimensions,
the dimensions are reversed.
"""
function fortran_column_major(A::Array{T, N}) where {T, N}
    if N == 1
        return A
    elseif N == 2
        return permutedims(A, (2, 1))
    else
        # Reverse all dimensions
        return permutedims(A, N:-1:1)
    end
end

"""
    julia_row_major(A::Array{T, N}) where {T, N}

Convert Fortran column-major array to Julia row-major order.

This is the inverse of fortran_column_major.
"""
function julia_row_major(A::Array{T, N}) where {T, N}
    return fortran_column_major(A)  # Same operation (reverse dimensions)
end

end # module FortranBinaryIO
